Skip to content

Mention how non-public fields of inherited classes are serialized #47148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

adamsitnik
Copy link
Member

@adamsitnik adamsitnik commented Jul 9, 2025

Repro:

using System.Formats.Nrbf;
using System.Runtime.Serialization.Formatters.Binary;

namespace NrbfBestPractices
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using MemoryStream payload = new();
#pragma warning disable SYSLIB0011 // Type or member is obsolete
            BinaryFormatter formatter = new();
#pragma warning restore SYSLIB0011 // Type or member is obsolete
            formatter.Serialize(payload, new Derived(42)
            {
                Text = "Hello, World!",
            });
            payload.Position = 0;

            ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
            Derived decoded = new(rootRecord.GetInt32("Base+integer")) // HERE
            {
                Text = rootRecord.GetString(nameof(Derived.Text)),
            };
            Console.WriteLine($"Integer: {decoded.Integer}, Text: {decoded.Text}");
        }
    }

    [Serializable]
    public class Base
    {
        private int integer;

        public int Integer => integer;

        public Base(int _)
        {
            integer = _;
        }
    }

    [Serializable]
    public class Derived : Base
    {
        public string Text;
        public Derived(int integer) : base(integer)
        {
        }
    }
}

Source of confusion:

https://github.com/dotnet/runtime/blob/513ff1acc981118e4b981e965dce614c3b8770f5/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/FormatterServices.cs#L42-L44

https://github.com/dotnet/runtime/blob/513ff1acc981118e4b981e965dce614c3b8770f5/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/FormatterServices.cs#L68

https://github.com/dotnet/runtime/blob/513ff1acc981118e4b981e965dce614c3b8770f5/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/SerializationFieldInfo.cs#L21


Internal previews

📄 File 🔗 Preview link
docs/standard/serialization/binaryformatter-migration-guide/functionality-reference.md BinaryFormatter functionality reference

@adamsitnik adamsitnik requested a review from jeffhandley July 9, 2025 11:13
@adamsitnik adamsitnik requested review from gewarren and a team as code owners July 9, 2025 11:13
@dotnetrepoman dotnetrepoman bot added this to the July 2025 milestone Jul 9, 2025
Copy link
Member

@GrabYourPitchforks GrabYourPitchforks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this deserves a code sample showing how things work.

For example:

[Serializable]
public class BaseClass {
    public int PublicIntField;
    private int NonPublicIntField;

    public BaseClass(int publicIntFieldValue, nonPublicIntFieldValue) {
        PublicIntField = publicIntFieldValue;
        NonPublicIntField = nonPublicIntFieldValue;
    }
}

[Serializable]
public class DerivedClass : BaseClass {
    public string PublicStringField;
    private string NonPublicStringField;

    public DerivedClass(int publicIntFieldValue, int nonPublicIntFieldValue)
        : base(publicIntFieldValue, nonPublicIntFieldValue) {
        PublicStringField = PublicIntField.ToString();
        NonPublicStringField = NonPublicIntField.ToString();
    }
}

Then show an example of serializing both a BaseClass and a DerivedClass, including what values are contained within each payload.

@@ -20,7 +20,7 @@ The <xref:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter> was fi

## Member names

In most common scenario, the type is annotated with `[Serializable]` and the serializer uses reflection to serialize **all fields** (both public and non-public) except those that are annotated with `[NonSerialized]`. By default, the serialized member names will match the type's field names. This historically led to incompatibilities when even private fields are renamed on `[Serializable]` types. During migrations away from BinaryFormatter, it becomes necessary to understand how serialized field names were handled and overridden.
In most common scenario, the type is annotated with `[Serializable]` and the serializer uses reflection to serialize **all fields** (both public and non-public) except those that are annotated with `[NonSerialized]`. By default, the serialized member names of public fields will match the type's field names. For non-public fields defined in inherited classes, the member names consist of inherited type name, a `+` sign and the field name (`$InheritedClassName+$nonPublicFieldName`). Serializing field names has historically led to incompatibilities when even private fields are renamed on `[Serializable]` types. During migrations away from BinaryFormatter, it becomes necessary to understand how serialized field names were handled and overridden.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In most common scenario, the type is annotated with `[Serializable]` and the serializer uses reflection to serialize **all fields** (both public and non-public) except those that are annotated with `[NonSerialized]`. By default, the serialized member names of public fields will match the type's field names. For non-public fields defined in inherited classes, the member names consist of inherited type name, a `+` sign and the field name (`$InheritedClassName+$nonPublicFieldName`). Serializing field names has historically led to incompatibilities when even private fields are renamed on `[Serializable]` types. During migrations away from BinaryFormatter, it becomes necessary to understand how serialized field names were handled and overridden.
In the most common scenario, the type is annotated with `[Serializable]` and the serializer uses reflection to serialize **all fields** (both public and non-public) except those that are annotated with `[NonSerialized]`. By default, the serialized member names of public fields will match the type's field names. For non-public fields defined in inherited classes, the member names consist of inherited type name, a `+` sign, and the field name (`$InheritedClassName+$nonPublicFieldName`). Serializing field names has historically led to incompatibilities when even private fields are renamed on `[Serializable]` types. During migrations away from BinaryFormatter, it became necessary to understand how serialized field names were handled and overridden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants